package pl.ydp.components.mathcell.mathrecognizer.geometry
{
	import flash.geom.Point;
	import flash.geom.Rectangle;
	
	import pl.ydp.components.mathcell.mathrecognizer.Debug;
	import pl.ydp.components.mathcell.mathrecognizer.engine.Parameters;
	import pl.ydp.components.mathcell.mathrecognizer.geometry.classification.PrimitiveSubstanceType;
	import pl.ydp.components.mathcell.mathrecognizer.geometry.extractor.PrimitiveExtractor;
	import pl.ydp.components.mathcell.mathrecognizer.geometry.extractor.features.ShapeFeatures;
	import pl.ydp.components.mathcell.mathrecognizer.geometry.extractor.quickshapes.QuickShape;
	import pl.ydp.components.mathcell.mathrecognizer.geometry.extractor.quickshapes.QuickShapeType;
	import pl.ydp.components.mathcell.mathrecognizer.geometry.optimizer.PrimitivesOptimizer;
	
	public class MathShape
	{
		// -------------------- CONSTRUCTOR--------------------
		
		public function MathShape(_pointsSet:Array /* _points - an array of arrays of points */)
		{
			var pointsSet:Array = new Array();
			var tmpArr:Array = new Array();
			var tmpPt:Point = new Point();
			
			for each (tmpArr in _pointsSet){
				pointsSet.push(new Array());
				for each (tmpPt in tmpArr){
					pointsSet[pointsSet.length-1].push(tmpPt.clone());
				}
			}
			
			if (!pointsSet[0] is Array  ||  pointsSet.length == 0){
				primitives = new Array();
				field = new Rectangle();
				return;
			} else if (!pointsSet[0][0] is Number){
				primitives = new Array();
				field = new Rectangle();
				return;				
			}
			
			var primitivesArray:Array = new Array();
			var pointsPrepared:Array = new Array();
			var prim:Primitive;
			var p:Number;
			
			findField(pointsSet);

			for (p = 0 ; p < pointsSet.length ; p ++){
				pointsSet[p] = normalizePoints(pointsSet[p]);
			}
			
			
			for (p = 0 ; p < pointsSet.length ; p ++){
				pointsSet[p] = decimatePoints(pointsSet[p]);
			}
			
			
			findField(pointsSet);
			
			for (p = 0 ; p < pointsSet.length ; p ++){
				pointsSet[p] = centerPoints(pointsSet[p]);
			}
			
			findField(pointsSet);
			
			
			for (p = 0 ; p < pointsSet.length ; p ++){
				pointsSet[p] = filterPointsAll(pointsSet[p]);
			}
			
			for (p = 0 ; p < pointsSet.length ; p ++){
				pointsSet[p] = interpPoints(pointsSet[p]);
			}
			
			for (p = 0 ; p < pointsSet.length ; p ++){
				pointsSet[p] = filterPoints(pointsSet[p], false);
				pointsSet[p] = filterPoints(pointsSet[p], false);
				pointsSet[p] = filterPoints(pointsSet[p], true);
			}
			
			findField(pointsSet);
			
			for each (var currPoints:Array in pointsSet){
				primitivesArray.push( findPrimitives( currPoints ) );
			}
			
			primitives = new Array();
			
			for each (var prims:Array in primitivesArray){
				for each (prim in prims){
					primitives.push(prim);
				}				
			}
			
			primitives = PrimitivesOptimizer.optimize(primitives);
			
			for each (prim in primitives){
				prim.findChildren();
			}
			
			features = new ShapeFeatures(primitives);
			
			
			findField();
			
		}
		
		
		// -------------------- PRIVATE FIELDS-------------------- 
		
		public var primitives:Array;
		public var field:Rectangle;
		
		public var features:ShapeFeatures;
				
		
		// -------------------- PRIVATE METHODS-------------------- 
		//------------------------------------------------------------------
		
		private function findPrimitives(points:Array):Array{
			
			Debug.saveLog(new Array(), "function FIND PRIMITIVES ()");
			
			return vectors2primitives(points2vectors(points));
		}
		

		
		//-------------------- WORK METHODS-------------------- 
		
		private function findField(data:Array = null):void{
			
			var min:Point;
			var max:Point;
				
			if (data != null){
			
				min = new Point(Infinity, Infinity);
				max = new Point(-Infinity, -Infinity);
				
				for each (var points:Array in data){
					for each (var currPoint:Point in points){
						
						if (currPoint.x > max.x){
							max.x = currPoint.x;}
						if (currPoint.y > max.y){
							max.y = currPoint.y;}
						if (currPoint.x < min.x){
							min.x = currPoint.x;}
						if (currPoint.y < min.y){
							min.y = currPoint.y;}
								
					}  // end for each currPoint
					
				}// end for each points
				
			} else {
				
				min = new Point(Infinity, Infinity);
				max = new Point(-Infinity, -Infinity);
				
				for each (var prim:Primitive in primitives){
					
						if (prim.field.right > max.x){
							max.x = prim.field.right;}
						if (prim.field.bottom > max.y){
							max.y = prim.field.bottom;}
							
						if (prim.field.x < min.x){
							min.x = prim.field.x;}
						if (prim.field.y < min.y){
							min.y = prim.field.y;}
					
				}// end for each points
								
			}
			
			field = new Rectangle(min.x, min.y, max.x - min.x + 1, max.y - min.y + 1);
			
		}
		
		//------------------------------------------------------------------
		
		
		private function normalizePoints(points:Array):Array{
						
			if (false && points.length > 0){
				
				var scale:Number=1;
				
				if (field.width > Parameters.CHAR_WIDTH){
					scale = Parameters.CHAR_WIDTH/field.width;
				}
				
				if (field.height*scale > Parameters.CHAR_HEIGHT){
					scale = Parameters.CHAR_HEIGHT/field.height;
				}
				
				scale *= Parameters.CHAR_SIZE_COEFF;
				
				for each (var currPoint:Point in points){
					currPoint.x *= scale;
					currPoint.y *= scale;
				}		
				
			}
			
			Debug.saveLog(points, "points (normalized)    in private function normalizePoints()");
			
			return points;	
		
		}
		
		//------------------------------------------------------------------
		
		private function decimatePoints(points:Array):Array{
			
			var pointsDecimated:Array = new Array;
			const SURVEY_WIDTH:Number = 3;
			
			pointsDecimated.push(points[0]);
			
			var x:Number = Parameters.MIN_PURE_VECTOR_LENGTH;
			
			var angle0:Number;
			var angle1:Number;
			
			for (var i:Number = 1 ; i < points.length - 1 ; i ++ ){
				angle0 = Angle.points2degrees( pointsDecimated[pointsDecimated.length-1] , points[i] );
				angle1 = Angle.points2degrees( points[i], points[i+1] );
				
				if (i == 56){
					var asd:Number = 10;
				}
				
				// eliminate stairs
				//
				//  *  *               *
				//     *  *      -->      *
				//        *  *               *
				//
				
				if (i < points.length - 2){
					if (Math.round(Angle.differenceAbs(angle0, angle1)) == AngleTrend.ANGLE_RIGHT   && 
						Math.round(Angle.differenceAbs(angle0, Angle.points2degrees( points[i+1], points[i+2] ) )) == AngleTrend.ANGLE_ZERO){
						continue;
					}
				}
				
				
				if (Point.distance(points[i], pointsDecimated[pointsDecimated.length-1]) > Parameters.MIN_PURE_VECTOR_LENGTH  ||  
					(Angle.differenceAbs(angle0, angle1) >= AngleTrend.THRESHOLD_CURVE-1)  ||  
					(Angle.differenceAbs(angle0, angle1) > AngleTrend.STILL)){
					pointsDecimated.push(points[i]);
				}
			}
			
			if (points.length > 1){
				pointsDecimated.push(points[points.length-1]);
			}
			
			Debug.saveLog(pointsDecimated, "points (decimated)    in private function decimatePoints");
			
			return pointsDecimated;
		}
		
		
		//------------------------------------------------------------------
		
		
		private function centerPoints(points:Array):Array{
				
			
			if (false && points.length > 0){
			
				var offset:Point = new Point((Parameters.CHAR_NORMALIZED_WIDTH - field.width)/2, (Parameters.CHAR_NORMALIZED_HEIGHT - field.height)/2);
				
				for each (var currPoint:Point in points){
					currPoint.x = (currPoint.x - field.x + offset.x);
					currPoint.y = (currPoint.y - field.y + offset.y);
				}
			}
			
			Debug.saveLog(points, "points (centered)    in private function centerPoints");
			
			return points;
		}
		
		//------------------------------------------------------------------
		
		private function filterPointsAll(points:Array, saveDebug:Boolean = true):Array{
			
			const A:Number = 5;
			const B:Number = 2;
			const BASE:Number = 3;
			const SUM_AB:Number = A + B*2;	
			
			var p:Number;
			var pointNew:Point;	
			var pointsFiltered:Array = new Array;
			
			if (points.length < BASE){
				return points;
			}
			
			// beginning
			
			for (p = 0 ; p < Math.floor(BASE/2) ; p ++ ){
				pointsFiltered.push(points[p].clone());
			}
			
			// body
			
			for (p = Math.floor(BASE/2) ; p < points.length - Math.floor(BASE/2) ; p ++ ){
					pointNew = new Point();
					pointNew.x = ( A * points[p].x + B * (points[p-1].x + points[p+1].x)) / SUM_AB;
					pointNew.y = ( A * points[p].y + B * (points[p-1].y + points[p+1].y)) / SUM_AB;
					pointsFiltered.push(pointNew.clone()); 					
			}
			
			// ending
			
			for (p = points.length - Math.floor(BASE/2) ; p < points.length ; p ++ ){
				pointsFiltered.push(points[p].clone());
			}
			
			if (saveDebug){
				Debug.saveLog(pointsFiltered, "points (filtered)    in private function filterPoints");	
			}
			
			return pointsFiltered;
			
		}
		
		//------------------------------------------------------------------
		
		private function filterPoints(points:Array, saveDebug:Boolean = true, bigSteps:Boolean = false):Array{
						
			var BASE:Number = 7;
			
			var BASE_ABC:Number = 5;
			var BASE_AB:Number = 3;
			
			var A:Number = 5;
			var B:Number = 3;
			var C:Number = 2;
			var D:Number = 1;
			var SUM:Number = A + B*2 + C*2 + D*2;
			const SUM_AB:Number = A + B*2;
			const SUM_ABC:Number = A + B*2 + 2*C;					
			
			var p:Number;
			var pm:Number;
			
			var filterMask:Array = new Array;
			var pointsFiltered:Array = new Array;
			var pointNew:Point = new Point;
			
			var stats:SerieStatsReport = new SerieStatsReport;
			var statsFar:SerieStatsReport = new SerieStatsReport;
			
			//const FILTER_CURVE_THRESHOLD:Number = AngleTrend.THRESHOLD_CURVE*1.5;
			const FILTER_CURVE_THRESHOLD:Number = AngleTrend.THRESHOLD_CURVE;
			
			if (points.length < BASE){
				return points;
			}
						
			for (p = 0 ; p < Math.floor(BASE/2) ; p ++ ){
					filterMask.push( true );
			}
				
			for (p = Math.floor(BASE/2) ; p < points.length - Math.floor(BASE/2) ; p ++ ){
				var angleSide0:Number = Angle.points2degrees(points[p-1],points[p]);
				var angleSide1:Number = Angle.points2degrees(points[p],points[p+1]);
				
				var angleDiff:Number = Angle.differenceAbs(angleSide0,angleSide1);
				
				if (angleDiff < FILTER_CURVE_THRESHOLD){
					filterMask.push( true );
				} else {
					filterMask.push( false );	
				}
				
			
			}
			
			// apply correction
			
			var nextToProcess:Number = 0;
			
			for (p = 0 ; p < Math.floor(BASE/2) ; p ++ ){
				pointsFiltered.push(points[p].clone());
			}
				
			for (p = Math.floor(BASE/2) ; p < points.length - Math.floor(BASE/2) ; p ++ ){
				
				pointNew = new Point;
				
				if (filterMask[p-3]  &&  filterMask[p+3]  &&
					filterMask[p-2]  &&  filterMask[p+2]  &&
					filterMask[p-1]  &&  filterMask[p+1]  &&
					filterMask[p]){
					
					pointNew.x = ( A * points[p].x + B * (points[p-1].x + points[p+1].x) + C * (points[p-2].x + points[p+2].x) + D * (points[p-3].x + points[p+3].x) );
					pointNew.y = ( A * points[p].y + B * (points[p-1].y + points[p+1].y) + C * (points[p-2].y + points[p+2].y) + D * (points[p-3].y + points[p+3].y) );
					
					pointNew.x /= SUM; 
					pointNew.y /= SUM;
					
				} else if (filterMask[p-2]  &&  filterMask[p+2]  &&
					filterMask[p-1]  &&  filterMask[p+1]  &&
					filterMask[p]){
					
					pointNew.x = ( A * points[p].x + B * (points[p-1].x + points[p+1].x) + C * (points[p-2].x + points[p+2].x) );
					pointNew.y = ( A * points[p].y + B * (points[p-1].y + points[p+1].y) + C * (points[p-2].y + points[p+2].y) );
					
					pointNew.x /= SUM_ABC; 
					pointNew.y /= SUM_ABC;
					
				} else if (filterMask[p-1]  &&  filterMask[p+1]  &&
					filterMask[p]){
					
					pointNew.x = ( A * points[p].x + B * (points[p-1].x + points[p+1].x) );
					pointNew.y = ( A * points[p].y + B * (points[p-1].y + points[p+1].y) );
					
					pointNew.x /= SUM_AB; 
					pointNew.y /= SUM_AB;
					
				} else {
					pointNew = points[p].clone();
				}
				
				pointsFiltered.push(pointNew);
			}
			
			
			for (p = points.length - Math.floor(BASE/2) ; p < points.length ; p ++ ){
				pointsFiltered.push(points[p].clone());
			}
			
			if (saveDebug){
				Debug.saveLog(pointsFiltered, "points (filtered)    in private function filterPoints");	
			}
			
			
			return pointsFiltered;
		}
		
		//------------------------------------------------------------------
		
		private function findPointsStats(points:Array):SerieStatsReport{
			
			var ssr:SerieStatsReport = new SerieStatsReport();
			var angleDiffs:Array = new Array();
			var currAngleDiff:Number;
			ssr.prepareForFilling();
			
			for (var i:Number = 0  ; i < points.length - 2 ; i ++ ){
				currAngleDiff = Angle.difference(Angle.points2degrees(points[i],points[i+1]), Angle.points2degrees(points[i+1],points[i+2]));
				
				ssr.mean += currAngleDiff;
				
				if (currAngleDiff > ssr.max){
					ssr.max = currAngleDiff;
				} 
				if (currAngleDiff < ssr.min){
					ssr.min = currAngleDiff
				}
				
				angleDiffs.push(currAngleDiff)
			}
			
			ssr.mean /= (points.length - 2);
			
			return ssr;
			
		}
		
		//------------------------------------------------------------------
		
		private function isBetween(base:Number, side0:Number, side1:Number, marginCoeff:Number):Boolean{
			
			var margin:Number;
			
			if (side1 > side0){
				
				margin = (side1 - side0)*marginCoeff;
				return (base >= side0-margin  &&  base <= side1+margin);
				
			} else {
				
				margin = (side0 - side1)*marginCoeff;
				return (base >= side1-margin  &&  base <= side0+margin);
				
			}
		}
		
		//------------------------------------------------------------------
		
		private function interpPoints(points:Array):Array{
			
			var pointsInterpd:Array = new Array();
			var pointsOriginal:Array = new Array();
			var IVL:Number = Parameters.INTERP_VECTOR_LENGTH;
			var VECTOR_LENGTH_SEEK_STEP:Number = Parameters.CHAR_SIZE_COEFF/4;
			
			for (var k:Number = 0 ; k < points.length - 1 ; k ++){
				
				if (Point.distance(points[k], points[k+1]) < Parameters.MIN_VECTOR_LENGTH){
					continue;
				}
				
				if (k == 17){
					var asd:Number = 123;
				}
				
				pointsInterpd.push(points[k].clone());
				pointsOriginal.push(points[k].clone());
				
				var distance:Number = Point.distance(points[k],points[k+1]);
								
				if (distance > IVL*2){
					
					var A:Number = (points[k+1].y - points[k].y) / (points[k+1].x - points[k].x);
					var B:Number = points[k].y - A * points[k].x;
					var signum:Point = new Point();
					signum.x = (points[k+1].x - points[k].x > 0) ? 1 : -1;  
					signum.y = (points[k+1].y - points[k].y > 0) ? 1 : -1;
					var pointTest:Point = new Point();
					var test:Number;
					
					test = 0;
					
					var tail:Number = (distance/IVL) / Math.floor(distance/IVL - 1); 
					
					for (var d:Number = 1 ; d < Math.floor(distance/IVL)-1 ; d ++ ){
						
						// iteracyjne poszukiwanie punktu interpolacji
						
						for (test ++ ; test < distance/VECTOR_LENGTH_SEEK_STEP; test++ ){
							if (Math.abs(A) == Infinity){
								// dx == 0
								pointTest.y = points[k].y + test * signum.y * VECTOR_LENGTH_SEEK_STEP;
								pointTest.x = points[k].x;
							} else if (Math.abs(A) < 1){
								// dodajemy do x
								pointTest.x = points[k].x + test * signum.x * VECTOR_LENGTH_SEEK_STEP;
								pointTest.y = A * pointTest.x + B;
							} else {
								// dodajemy do y
								pointTest.y = points[k].y + test * signum.y * VECTOR_LENGTH_SEEK_STEP;
								pointTest.x = (pointTest.y - B) / A;
							}
							if (Point.distance(points[k],pointTest) >= IVL * tail * d){
								break;
							}
							
						}  // end for (test...
						
						pointsInterpd.push(pointTest.clone());
						pointsOriginal.push(points[k].clone());
						
					}  //  end for (d...
					
				}  // end if (distance > IVL*2) {
				
			}   // end for k...
			
			pointsInterpd.push(points[points.length-1].clone());
			pointsOriginal.push(points[points.length-1].clone());
			
			//Debug.saveLog(pointsOriginal, "pointsOriginal (not-interpoled)    in private function interpPoints");
			
			Debug.saveLog(pointsInterpd, "pointsInterpd (interpoled)    in private function interpPoints");
			
			return pointsInterpd;
		}
		//------------------------------------------------------------------
		
		private function points2vectors(points:Array):Array{
			
			var euclideans:Array = new Array();
			
			if (points.length == 1){
				euclideans.push(new EuclideanVector(points[0], points[0]));
			}
			
			for (var i:Number = 0 ; i < points.length-1 ; i ++ ){
				euclideans.push(new EuclideanVector(points[i], points[i+1])) ;
			}
			
			return euclideans;
		}
		
		
		//------------------------------------------------------------------
		
				
		private function vectors2primitives(vectors:Array):Array{
			
			var k:Number;
			var outputPrimitives:Array = new Array;
			var quickShape:QuickShape;
			var extractor:PrimitiveExtractor;
			var substances:Array = new Array; 
			
			quickShape = new QuickShape(vectors);
			
			if (quickShape.type == QuickShapeType.UNKNOWN){
				extractor = new PrimitiveExtractor(vectors);
				substances = extractor.substances;
			} else {
				substances.push(quickShape.substance);
			}
			
			
			for (k = 0 ; k < substances.length ; k ++ ){
				outputPrimitives.push( new Primitive(vectors.slice(substances[k].begin, substances[k].end+1), substances[k].type, substances[k].helixCount) );
			}
			
			if (substances.length == 0){
				outputPrimitives.push( new Primitive(vectors, PrimitiveSubstanceType.UNKNOWN) );
			}
						
			
			return outputPrimitives;
		}
		
		

		//------------------------------------------------------------------
		
		

		
	}
}